avatar

目录
317 - cpp 类级别数据

C++ 类级别数据

🧑‍💻 User

In this lecture, we’re going to see how we can set up static member variables in our C plus plus program.We have seen that static member variables are variables that are not really attached to any class object.They are attached or tied to the class blueprint itself.And we have seen that they are really useful in that.For example, they can allow you to keep track of how many objects for your class are created.Let’s see how we can create them.Here is a simple example.We have our class point.And we have a few member variables inside, but the member variable of interest here is the point countmember variable here it’s type is size T, but notice that we have marked it as static here.What this is going to do, it’s going to tie this member variable to the class blueprint itself.The member variable is not going to be tied to any point object, but any point object is going to haveaccess to this member variable, but the lifetime of the member variable will be tied to the class blueprintitself.In fact, static member variables are even initialized before you create any object for the class.This may sound counterintuitive, but I am going to show you this in a minute when we head to VisualStudio code.For now, just know the syntax of our static member variable.The syntax is thus you prepend the static keyword to the declaration of your member variable.And from that point on it’s going to be a static member variable.Now you see that we haven’t really initialized our member variable here, so now it’s probably goingto contain some junk and we don’t want to do this.But Cplusplus doesn’t allow initializing static member variables like this in the header file.So if we use a pair of curly braces after this and use brace initialization, we’re going to get a compilererror.What c plus plus allows is to go in some CPP file and initialize this member variable and the linkerknows to go and hunt for this initialization.In any translation unit that you have in your application.The syntax is really simple.You say the class that the member variable belongs to using this scope resolution.Operator And then you say the variable name.After this, you can then use curly braces to initialize your member variable to whatever it is youwant.This is going to set up our static member variable and we can use it in our C plus plus program.Again, note that static member variables are initialized before your program runs.In fact, they are even initialized before the main function gets to run because they are special inthat way they are not tied to any object, so they need to be initialized before any object is createdin C plus plus.And we’re going to try and prove that when we hit Visual Studio code in a minute.But before we go, I want to introduce you to a new function or member function we’re going to introducein our class point.It is going to be a function to compute the length of a line, and a line is going to be made up oftwo points.So we’re going to use this formula you see down here and we’re going to take the X position for thesecond point and minus the X position for the first point, we’re going to elevate that to the powerof two here and we’re going to do the same computation on Y, and then we’re going to do a square rootof the result that we get inside here.So this is how we can compute the distance between two points.And we’re going to do that in our C plus plus program.This is one way I thought of we could make our points a little bit interesting because what we havebeen so far can really be boring knowing what we know about C plus plus.Now we want to make things a little interesting.Okay, so this is a possible representation of two points.We have the origin point, which is at the address zero zero, and then we have a point, for example,which lives at an X position of ten and the Y position of five.Suppose this point here is at X of ten, and the point here is at Y of five.So if we go like this, this is going to be the coordinate of our point.But what we want to do is to compute the distance between the two points.So we want to do something like this.And now the distance between this point here and this point here.And the formula to do that is what we see here in this point.What we’re going to do is treat the ten here as our x two.And the zero here is going to be our x one.Then the five is going to be our Y two, and the zero here is going to be our Y one.Then we’re going to do the computation and what we will get is going to be the distance between thesetwo points.This is a formula you can use and the translating this in C plus plus we can do something like this.So we’re going to use the square root function we have in the C plus plus standard library.So this is basically going to be our square root.And then we’re going to take the X position for the second point and minus the X position for the firstpoint here.And then we’re going to elevate that to the power of two.We’re going to do the.Same computation for Y.And that’s what we have here.And this is going to give us the distance between two points.And we can do this in our C plus plus program.So now that we know this, let’s head over to Visual Studio code and play with static member variables.Okay.So here we are in our working folder.The current project is static member variables.We’re going to grab our template files pretty quick.We are going to put them in place and we are going to open this in Visual Studio code by dragging anddropping like we always do here.This is going to open our folder.We’re going to open the Main.cpp file and we’re going to clean it up a little bit.Let’s do that.We’re going to create our files for the point class that we are going to use here.We’re going to put in a header file point dot h, and we’re going to put in a CPP file point dot CPP.We are going to go in our header file and put in our class.I’m going to put it in because I don’t want to watch me type all these things.We have our include guards on top and we have the end of our include guards and inside we have a bunchof constructors.In here we see a two parameter constructor.We have a one parameter constructor that we can use to construct a point which has the same x and Ycoordinates.We have a default constructor and we have a copy constructor.We also have a destructor which is really not going to do anything because we don’t have any pointermember variables in here.But if you happen to have any pointer member variables and you are doing some dynamic memory allocationin the constructors, you’re going to use this destructor here, but we’re going to put it in here justto be complete.So down here we have the meat of this example.Here we have a static member variable.This is going to be a member variable whose association is going to be with the class itself.It’s not going to be associated with any class object, but we’re going to prove this in a minute.For now, let’s head over in point CPP and try to implement many of these methods.I think we can actually grab them all.Let’s do that.We’re going to grab them all here and do them all one by one.And I would challenge you to try and implement these things yourself without watching anything becauseyou already have the tools to do that.So we’re going to include point dot H, our header, and then we’re going to implement this constructorhere.So we’re going to put in our scope resolution operator and we’re going to do our initializer list.Our member variables are called M, X and M Y.So we’re going to do that.We’re going to say M x is going to be initialized with X.Let’s use brace initialization here and m y is going to be initialized with Y.Let’s do that.And once we have this, we can jump into the body of our constructor here and it is already done.It is initializing our member variables.Let’s do the second one.So we’re going to put in our scope resolution.Operator And for this constructor we are going to delegate the construction to the first constructorwe did here because we don’t want to repeat ourselves.So what we’re going to do is delegate to the other constructor and we’re going to give it x y chordin the position for the x coordinate.And this is going to do what we want, and then we’re going to implement this constructor here.This constructor is going to be initializing our member variables.The member variables are called M, X and Y.So let’s go down here and put in our scope resolution.Operator.We’re going to say point, and then we’re going to use an initializer list to initialize our membervariables.We’re going to say m X, we’re going to use braced initialization here.So we’re going to say X and we’re going to say M Y and put in our initializer to be Y Here, it’s notX and then we’re going to put in our body and our constructor here is basically done.Now the remaining constructors here will be delegating the construction work to the constructor thatwe just did here.This is going to avoid us to do the same job in the body.This constructor here is going to take over that heavy lifting and the others are just going to be usingit.The way we do that, we’re going to use constructor delegation.You already know that.So let’s do that here and we’re going to delegate to that constructor.We’re going to say point and then we’re going to pass the X and Y coordinate.This constructor.Here is a single parameter constructor which is going to pass the same value for X and Y.So we’re going to use our parameter here.We’re going to say x y chord for the Y coordinate and then we’re going to go in our body.This is not autocompleting because we didn’t say that this constructor is from the point class we forgot.Our scope resolution operator.Let’s do that pretty quick.And all these squiggly lines should go away in a minute.Let’s wait.Okay.They’re not going away.But if there’s a problem, we’re going to figure this out.We’re going to do the other constructor here, which is a default constructor.We’re going to put in our scope resolution operator, and we’re going to grab the comment here and putthat on top.I think this is going to do let’s align this properly and we’re going to put our initializer.It is going to be delegating to the other constructor and we’re going to put in default parameters wewant to use.We’re going to use zero zero.Let’s do that 0.0 because it’s a double and then we’re going to do the body.Okay, so this constructor here is done.Next we’re going to do our copy constructor, which is going to be doing member Wise copy.It’s not going to do anything special because we don’t have any pointer member variables.So this is going to be basically the same thing as the default constructor that is generated by thecompiler.But we want to do it ourselves.So let’s delegate and we’re going to say point M x and we should use brace initialization.Here I am mixing things up, so we’re going to say point M X and then we’re going to say point M y andwe’re going to put in our body.This should do what we want and we need the scope resolution operator here.Let’s do that.And the destructor is really not going to do anything.So we’re going to put our scope resolution operator and we’re just going to put a body.Let’s do that.We’re going to take out this comment, put that on top a little bit, and we’re going to align hereand we’re going to put the body in here.And the remaining function here is going to compute the distance to the origin.And we have seen the formula we can use to do that.So let’s go down and put in our body.And this function is going to belong to the point class.We’re going to tell that to the compiler using our scope resolution operator and we’re going to moveour return value before the scope resolution.Operator Let’s be careful about this.We are going to go on top and include the math library from the C plus plus standard library.We’re going to grab Cmath and then we’re going to go down and put in the body for our function here.And it’s basically going to return the distance between the current object, which is the current pointto the origin.Again, if you have forgotten, this is the formula we use to compute the distance between points.Our point is going to be this point here and we’re going to be computing the distance to the originand we’re going to get to that as a double value.Okay, so by now our functions should be all implemented.And what we can do is try and build this program and see if we don’t have any problems.Let’s use GCC for that.We’re going to build with GCC and we’re going to have a problem and it’s going to say x, y not declaredin this scope point CPP at line nine.Let’s go there and do that x, y, underscore code.This is the problem here.We’re going to fix this and then we’re going to try and build again.Now the build is good, our class is working and we can create objects of this class and really seethat the memory locations for the member variables are different from different objects.So to really see this, we’re going to put in a print function, we’re going to make it public and we’regoing to say it’s going to be const because it’s not going to modify this class here or the member variablesfrom this class.And we’re going to say Stdcout point and we’re going to say address of X, That’s what we want to printhere.And we’re going to put a comma here and say address.That’s what we want to see and we are going to print this out.We’re going to say address of M y, and we’re going to put our closing square bracket.I think we should put one here.We don’t need a colon here.I think this is going to be better.And then we’re going to put our new line character.Now we can head over to Main and really play with this.We’re going to include our point class and we’re going to go down and create two points.We’re going to say point P one and we’re going to put in two values ten and 20.Why not?And we’re going to say point P two and we’re going to say 21.8 and 40.6.Why not?And then we’re going to say P one print info, we’re going to print the information and P two printable.Okay, So let’s build.What will we see when we print this information?We’re going to see the addresses for the X and Y member variables for this.Objects.And what we want to prove is that each object is going to have its own member variables and they aregoing to be completely independent of other objects.So let’s build.We’re going to build with GCC.And if we go up, you see that the build is good.Now we can clear and run Rooster.If we do that, you’re going to see that.Okay, so you see the output is saying zero for the x member variable for the first point and it issaying D8 for the second member variable of the first point.If we go to the second point, you see that we have DC zero and DC eight.You see the addresses are different.So the member variables are completely different because they live in different memory locations.So this is working fine.If you want variables to be constrained to objects, the default way we have to set up a member variablesis going to work.But if you want member variables to be associated with the class itself, you’re going to need to dosomething else.And to be able to achieve that, we will have to mark our member variables as static.So we have a member variable which is static here.Its type is size T, the name of the variable is M point count.So once you have it declared like this, the variable is not going to be properly initialized.If you want to initialize it, you will have to do it in a CPP file because if you try to do that inthe class definition like this, we’re going to get a compiler error.C plus plus doesn’t allow this.Okay, So you see, we do this, we get a squiggly line.If we try to build with GCC, let’s do that.We’re going to get a compiler error and the compiler error is saying in class initialization of non-conststatic member point here is not allowed.This is what the error is saying here.We’re not allowed to do this.If we want to initialize our static member variable, we will need to do this in the CPP file and wecan really do it in any CPP file.But we have pointed at CPP here, so we can just do that here.We’re going to say initialize static member var and the way you do that, you say the class where themember variable belongs and then you’re going to say the name of the member variable and then you’regoing to initialize it with whatever you want to initialize that with.Let’s say we want this to be zero by default and we’re going to put a semicolon here and in front weneed to say the type of this thing.So it is of size t type and this is the syntax we use to initialize static member variables.If we do this, this is going to be properly initialized.Now we have our static member variable.It is initialized in a CPP file.How do we use it in our C plus plus program?Well, because this guy is public, we can really use a convenient syntax to access this.We can even access this before any object is created for a point.So let’s go back in the main CPP file and really try to do this.We have already used this point.I don’t think we need them anymore, so we’re going to take them out and we’re going to say print thepoint count in our program and we are going to say Stdcout, like we say always and we’re going to saypoint count and notice the syntax we’re going to use.We’re going to say point and then end point count.This is going to give us direct access to our static member variable.And this is really cool.Notice that we are able to do this before any point object is created.And this is really cool.This proves that the static member variable is initialized even before the main function runs becausethis is what C plus plus does.We are able to get to the point count before we even create any point object.Notice that no constructor has been called.We didn’t set up any point object in Main.We just printed the point count and we’re going to see that if we print this, we’re going to get zerobecause that’s what we initialized our static variable with and this is really cool.So let’s try and build our program.We’re going to use GCC to do that.You see build finished successfully.So this is cool.We can clear and run.Rooster point count is zero.We are able to access our static member variable and it is attached to the class itself.It’s not attached to any member variable.Now let’s try and change what we initialize this guy with so that you really believe that this is goingto come here.The value we print, we’re going to build again, let’s use GCC and the build is good.We’re going to clear.If we run Rooster, we’re going to see the value we initialized our static variable with and this iswhat static member variables are really about.They are variables that are attached to the class itself and they are not attached to any object ofyour class.Now let’s use this member variable to really do some cool.We are going to go in our heavy lifter constructor and any point and after we create any point object,we’re going to increment our static variable to properly keep count of how many point objects we havein our program.So we’re going to increment and point count, and whenever the destructor runs, our point object willdie.So we’re going to decrement the point count.So we’re going to go in our destructor and we’re going to minus, minus M point out.And now we have the option and now we have the capability to really get how many points we have createdin our programs.We can print the point count here.It’s going to be zero because we don’t have any point objects created.But now we can create a new point object.Let’s put in ten and 20, and if we print the point count again, we’re going to see that we’re goingto get a one.Let’s do that.We’re going to build with GCC.The world is good, so we can clear and run.Rooster.You see point count 33.Why do we have 33 here?We need to go back and change what we initialize our point count with.So we’re going to initialize this to zero and then we’re going to run.Why aren’t we incrementing here when we call our constructor here?Let’s say point constructor called here so that we know that the constructor is being called and we’regoing to pass double parameters here.Let’s make sure we pass what the compiler expects.So we’re going to pass 20.4 here, for example, and then we’re going to build with GCC.The build is going to be good.We can clear and run Rooster.Now we see that the point count is zero.Why is this?After we create a point here, we’re going to increment point count and we should get a one.After we do this, we can print the point count here and we’re going to get zero because at this pointwe don’t have any point objects created.But we can create a point here.We’re going to say point P one and we’re going to put in 10.0, for example, and a 20 point.And if we do this and print the point count again, let’s copy this and we’re going to put this downand we’re going to make this a semicolon, not a colon.And if we try to build, we’re going to see that here.We’re going to get the zero run.But the moment we hit this point here, we’re going to initialize, but the moment we hit line ninehere, we’re going to create a new point object.This is going to call our constructor here and it is going to increment M point count.If we print this, we’re going to get a one.Let’s be sure that our constructor is ridiculed.So we’re going to say point constructor calls and this is the constructor that is going to be calledregardless of how we Build our object, because all the other constructors are delegating the work toit.You notice this is what we are calling for.Any constructor that we have through constructor delegation and this is really cool.So if we print here, we’re going to get a zero.And if we print here, we’re going to get a one.This is what we expect.Let’s build and we’re going to build just fine.The build is good.We can clear and run Rooster and we’re going to see that for the first time.The count is zero.Now, point is one, and this is really cool.We can set up an array of points.We can do that.Let’s say, for example, zero point and we’re going to say point and we’re going to put in a pair ofsquare brackets and then we’re going to initialize this with a pair of points.Let’s put in, for example, one one.We can really do whatever we want with this.We’re going to put a point here which is going to call the default constructor.Notice that we are building these guys in place.You can really do this if you want.And then we’re going to put another point, which is going to call the one parameter constructor andwe’re going to put in four.Why not?Now?If we print the count for points, we’re going to get a good number for points.We have P one here, which is our first point.We have two points, three points and four points.So now if we print, we expect to get a four printed out and hopefully this is going to prove that weare really keeping track of the points that we create in our C plus plus program through the help ofour static member variable, which is really not attached to any object we create in the main functionhere.It’s just attached to the class itself.So we’re going to build, let’s use GCC to do that.The world is going to be good.We can clear and run, Rooster.Now you see that we have a few constructors called four times, and each of these constructor callsis going to increment count by one.And when all these points are created, we’re going to have four points in total.And this is really cool.Okay.Now that we have already played with our static variable, I wanted to make it super clear that it ispossible to really initialize this static variable in any translation unit or in any CPP file to beexact.Let’s go on the left here and create another CPP file.So we’re going to head to the Add new file button here and we’re going to say some other file and we’regoing to CPP.This is going to be a CPP file and it is in the wrong folder.So I think we can grab this and bring it out.Let’s see if this works and we have the file here and we can grab the initialization of our static variableand really move that there.So we’re going to save this, but to make this work, we need to include the point class, because atthis point, this line here is not going to make sense to the compiler because the compiler is not goingto know what the point is.So we’re going to include the point here.And at the moment we do this now the compiler is going to know what pointers and it is going to initializethe static variable that is declared in our class here.So notice that we don’t have this initialization in point dot CPP anymore, but if we build, this isgoing to build just fine.Let’s build with GCC.You’re going to see that the world is good and we can clear and run rooster and we’re going to get theoutput that we expect.Another thing I want to be super clear is that a static variable doesn’t belong to any object and wecan really modify our static variables if we want in any cast member functions.So this is going to be counterintuitive, but note that it is possible to modify the static variable.Remember the const keyword after a member function is going to prevent you from modifying any membervariable in the class or in the object because member variables are associated with objects.But a static variable is not associated to any object.It belongs to the class as a whole.So the const keyword in your member function is not really going to protect it.So to prove this, we’re going to go in our const member function here and we are, for example, goingto increment M point count, you’re going to see that this is going to build and some of you are goingto say, What are you doing here?You are trying to modify a member variable, but this member variable doesn’t belong to any point object.It belongs to the entire class as a whole.The const keyword here is going to prevent you from modifying any member variable that belongs to theobject.And when I say object, I mean object like P1 here.So if we go down here and for example, say 0.1, get point count, just calling the function here,we’re going to head to the body of this function.We’re going to increment and then we’re going to return.If we try to print what we return, let’s say size t result and assign that to and give that the valuewe get from our function call here.And if we try to print that out, we’re going to get a five because we have already changed our membervariable through the call to this function here, get point count.I really hope you understand this.We’re going to build.And the world should be good.And if we print, the result should be five and it is a five here.And if we print the point, count through the syntax we used here, let’s do that.We’re going to put this here.We’re also going to get a five because this is the same variable.Let’s do that.We’re going to build with GCC.The world is going to be good.We can clear and run Rooster.Now we have a five here and this is really cool.Now, if you go back in our class, you’re going to notice that the static variable is in the publicscope.And I did this on purpose because I wanted the syntax.Here we have used saying point colon, colon and point count to access the static variable before anyobject is created.Work.I wanted this syntax here to work, but if you bring your static variable in the private scope, let’stry and take this out and put this back together with the other variables.We’re going to go back in main and we’re going to see that this is no longer working.The syntax here is only going to work if your static variable is living in the public scope.If you don’t have it in the public scope, you can access that through any function or member functionthat is returning that.Let’s take out this call here because you already know that you can modify static variables in castmember functions.So now we can have access to our variable through the member function here.If we want, we can do that.So let’s go back and really fix this.We don’t want this compiler errors, so we’re going to comment this out because at this point we don’thave any objects created.But the moment we have an object created, we can go through the object and get how many objects arecreated for that object.So here we’re going to say P one get point count, we’re going to call the function here or the memberfunction.We can take this out because we don’t need this anymore.And down here we’re going to say p one get point count and we’re going to print that for the first callhere.We’re going to get a one.We’re going to have a four here and we don’t need the second one here.So let’s take this out.If we do this, we’re going to get four printed out because we have four points created at this pointhere.We can copy this and go on top here and paste this in place.And at this point, we’re going to see a want to print it out because we only have one point created.Let’s build and really see this in action.We’re going to use GCC.The world should be good.We’re going to clear and run Rooster.Now we have zero point constructor called We have one point.We’re going to call the constructor three times because we have three points stored in our array hereand the total point count is going to be four as we see here.And we are going through the syntax here.So again, the message is if your static variable happens to be living in the private scope of yourclass, you’re not going to be able to use this syntax here to get access to your static variable.You have to go through some function to get it if you really want that.This is the message here.Okay.I think this is really all I had to share in this lecture.Raising your awareness about static variables.It should be clear that static variables or static member variables belong to the class as a whole.They don’t belong to any object of your class that you create.We have seen that we can use this syntax here to get access to static member variables if they happento live in the public scope of our class and if they live in the private scope, you have to go throughan object and use some member function that returns the static variable and that’s going to work.Also, we have seen that it is not possible to initialize your static member variable inside the class.If you try to do that, you’re going to get a compiler error.If you want to initialize that, you will have to go in some CPP file.And here and here we are doing that through a CPP file called some other file and the linker is goingto pick up this definition or initialization that we have here and it is going to work.This is really all I had to share.I hope you found this lecture interesting.We are going to stop here in this one.In the next one, we’re going to see a technique introduced in C plus plus 17 to allow us to initializeour static member variables inside the class declaration, if that’s what we want.So go ahead and finish up here and meet me there.


🤖 Assistant

好的,非常荣幸能为您讲解 C++ 中的静态成员变量(static member variables)。作为您学习 C++ 之旅的向导,我将尽力用最清晰、最易懂的方式来呈现这些知识。


C++ 静态成员变量:掌握类级别数据的精髓

摘要: 本课程将深入探讨 C++ 中静态成员变量的定义、用途、声明与初始化方法,以及如何访问它们。我们将通过详细的代码示例,理解静态成员变量与普通成员变量的区别,并揭示其在对象计数等场景中的强大作用。


1. 什么是静态成员变量?

在 C++ 中,我们通常会定义类(class)来创建对象(object)。每个对象都有自己独立的一套成员变量(member variables),就像每个学生都有自己的课本一样。但是,有时候我们需要一种特殊的变量,它不属于任何特定的对象,而是属于整个类本身。无论你创建了多少个该类的对象,甚至一个对象都没创建,这个变量都只有一个副本,并且所有对象共享这同一个副本。这种变量就是静态成员变量

关键概念:

  • 不依附于对象: 静态成员变量不存储在类的每个对象中。这意味着当你创建多个 Point 对象时,每个 Point 对象都会有自己的 m_xm_y 副本,但所有的 Point 对象将共享同一个 m_point_count 静态成员变量。
  • 依附于类本身: 它们是“类级别”(class-level)的变量,你可以把它们看作是与类的蓝图(blueprint)本身紧密绑定的数据。
  • 生命周期: 静态成员变量的生命周期与整个程序的生命周期相同。它们在 main 函数运行之前就被初始化,并在程序结束时才被销毁。
  • 用途: 静态成员变量非常适合用于跟踪类中所有对象共享的信息,例如:
    • 对象计数: 统计某个类有多少个对象被创建了,正如本例中 m_point_count 的作用。
    • 共享配置: 存储所有对象都需要的共同配置或常量。
    • 资源管理: 管理所有对象共享的唯一资源(例如,数据库连接池的大小)。

2. 如何声明静态成员变量?

声明静态成员变量非常简单,只需要在成员变量的类型前面加上 static 关键字即可。

语法:

cpp
1
2
3
4
5
class ClassName {
public:
// ... 其他成员 ...
static DataType static_member_variable_name; // 声明静态成员变量
};

示例(Point.h 文件中):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Point.h
#ifndef POINT_H
#define POINT_H

#include <cstddef> // for size_t

class Point {
private:
double m_x;
double m_y;
// 声明一个静态成员变量 m_point_count
// 它的类型是 size_t (通常用于表示大小或计数,是无符号整数)
// 注意:这里只是声明,不能在这里初始化!
static size_t m_point_count;

public:
// 构造函数
Point(double x, double y);
Point(double xy_coord);
Point();
Point(const Point& other);

// 析构函数
~Point();

// 成员函数
void print_info() const;
double distance_to_origin() const;

// 访问静态成员变量的静态成员函数
// 注意:这里也加上了 static 关键字
static size_t get_point_count();

// 用于演示 const 成员函数可以修改静态成员变量
void increment_static_count_via_const_func() const;
};

#endif // POINT_H

解释:

Point.h 文件的 Point 类定义中,我们声明了一个名为 m_point_countstatic size_t 类型变量。这个 static 关键字告诉编译器:m_point_count 不属于 Point 类的任何特定对象,而是属于 Point 类本身。

重要提示: 在类定义内部,你只能声明静态成员变量,而不能进行初始化(除非是 const static 整型或 C++17 inline 变量,这超出了本次课程范围)。如果你尝试在类定义内使用花括号 {} 或等号 = 进行初始化,编译器会报错。这是 C++ 的一个规则,因为类的定义通常放在头文件(header file)中,头文件可能被多个源文件(source file)包含,如果在头文件中初始化,可能会导致重复定义(multiple definitions)的问题。

3. 如何初始化静态成员变量?

由于静态成员变量不属于任何对象,它们的初始化方式也与普通成员变量不同。你必须在类定义之外,通常是在一个 .cpp 源文件中,为它进行定义和初始化。

语法:

cpp
1
2
// 在某个 .cpp 文件中
DataType ClassName::static_member_variable_name = initial_value;

示例(Point.cpp 文件中):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Point.cpp
#include "Point.h"
#include <iostream> // for std::cout, std::endl
#include <cmath> // for std::sqrt, std::pow

// 在 Point.cpp 中初始化静态成员变量
// 注意:这里需要再次指定类型 (size_t) 和所属的类 (Point::)
size_t Point::m_point_count = 0;

// 构造函数实现 (为了简洁,这里只展示关键部分,完整代码见下方)
Point::Point(double x, double y) : m_x(x), m_y(y) {
++m_point_count; // 每次创建对象,计数加一
std::cout << "Point(double, double) constructor called. Point count: " << m_point_count << std::endl;
}

Point::Point(double xy_coord) : Point(xy_coord, xy_coord) {
// 委托构造函数已经处理了 m_point_count 的增量
}

Point::Point() : Point(0.0, 0.0) {
// 委托构造函数已经处理了 m_point_count 的增量
}

Point::Point(const Point& other) : m_x(other.m_x), m_y(other.m_y) {
++m_point_count; // 复制构造也算创建新对象,计数加一
std::cout << "Point copy constructor called. Point count: " << m_point_count << std::endl;
}

// 析构函数实现
Point::~Point() {
--m_point_count; // 每次销毁对象,计数减一
std::cout << "Point destructor called. Point count: " << m_point_count << std::endl;
}

// 静态成员函数实现
size_t Point::get_point_count() {
return m_point_count;
}

// 演示 const 成员函数可以修改静态成员变量
void Point::increment_static_count_via_const_func() const {
// 尽管是 const 成员函数,但 m_point_count 不属于对象状态,所以可以被修改
++m_point_count;
std::cout << "Static count incremented via const member function. New count: " << m_point_count << std::endl;
}

// ... 其他成员函数实现 ...
void Point::print_info() const {
std::cout << "Point [m_x address: " << &m_x << ", m_y address: " << &m_y << "]" << std::endl;
}

double Point::distance_to_origin() const {
return std::sqrt(std::pow(m_x - 0.0, 2) + std::pow(m_y - 0.0, 2));
}

解释:

  • Point.cpp 文件中,我们写下了 size_t Point::m_point_count = 0; 这行代码。
    • size_t:表示 m_point_count 的类型。
    • Point:::这是作用域解析运算符(scope resolution operator),它告诉编译器 m_point_countPoint 类的成员。
    • = 0;:这是对 m_point_count 进行的初始赋值。
  • 初始化时机: 静态成员变量的初始化发生在 main 函数运行之前。这意味着即使你的程序没有创建任何 Point 对象,m_point_count 也会被正确地初始化为 0。链接器(linker))会负责在编译(compilation)过程中找到这个唯一的初始化定义。
  • 在构造函数和析构函数中更新: 为了实现对象计数,我们在 Point 类的所有构造函数中增加了 ++m_point_count;,并在析构函数中增加了 --m_point_count;。这样,每次创建 Point 对象时,计数就会增加;每次 Point 对象被销毁时,计数就会减少。

4. 如何访问静态成员变量?

访问静态成员变量有两种主要方式:

a. 通过类名直接访问 (推荐且常见):

如果静态成员变量是 public(公共)的,你可以直接使用类名和作用域解析运算符 :: 来访问它。这种方式最常用,也最能体现静态成员变量“属于类”的特性。

语法:

cpp
1
ClassName::static_member_variable_name

示例:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// main.cpp
#include "Point.h"
#include <iostream>

int main() {
// 在创建任何 Point 对象之前,访问静态成员变量
// Point::m_point_count (如果 m_point_count 是 public 的话)
// 由于我们将 m_point_count 设置为 private,我们需要通过静态成员函数访问
std::cout << "程序开始时,Point 对象的数量: " << Point::get_point_count() << std::endl; // 输出 0

Point p1(10.0, 20.0); // 调用构造函数,m_point_count 变为 1
std::cout << "创建 p1 后,Point 对象的数量: " << Point::get_point_count() << std::endl; // 输出 1

p1.print_info(); // 打印 p1 的成员变量地址

// 创建一个 Point 数组
Point points_arr[] = { Point(1.0, 1.0), Point(), Point(4.0) }; // 创建 3 个 Point 对象
std::cout << "创建数组后,Point 对象的总数量: " << Point::get_point_count() << std::endl; // 输出 4

// 演示 const 成员函数可以修改静态成员变量
p1.increment_static_count_via_const_func();
std::cout << "通过 const 成员函数修改后,Point 对象的总数量: " << Point::get_point_count() << std::endl; // 输出 5

// 访问数组中的元素
points_arr[0].print_info();
points_arr[1].print_info();
points_arr[2].print_info();

// 当 main 函数结束时,p1 和 points_arr 中的对象都会被销毁,m_point_count 会相应减少。
// 在这个示例中,总共有 5 个对象,程序结束时会调用 5 次析构函数。
// 如果程序在 main 结束前没有手动退出,最终 m_point_count 会再次回到 0。

return 0;
}

b. 通过对象访问 (不推荐,但有时需要通过成员函数间接访问):

你也可以通过类的任何一个对象来访问静态成员变量(但如果静态变量是 private 的,你必须通过一个公共的成员函数来访问)。虽然语法上允许 object.static_member_variable_name (如果 static_member_variable_namepublic),但这种方式容易让人误以为静态成员变量是某个特定对象的属性,从而造成误解。更推荐的做法是定义一个静态成员函数(static member function)来访问 private 静态成员变量。

静态成员函数:

  • 静态成员函数也使用 static 关键字声明。
  • 它们不接收隐式的 this 指针,因此它们不能访问非静态的成员变量或成员函数(因为非静态成员变量和函数是依附于对象的)。
  • 它们主要用于操作静态成员变量或执行与类本身相关的操作。

示例(Point.h 中声明,Point.cpp 中实现):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Point.h
class Point {
// ...
static size_t m_point_count; // private
public:
// ...
static size_t get_point_count(); // 静态成员函数
// ...
};

// Point.cpp
size_t Point::get_point_count() {
return m_point_count; // 静态成员函数可以访问静态成员变量
}

main.cpp 中,我们就是通过 Point::get_point_count() 来访问 m_point_count 的,因为 m_point_count 被声明为 private

5. 静态成员变量与 const 成员函数

这是一个容易混淆的点。const 成员函数(const member function)的目的是保证不会修改调用它的对象的任何非静态成员变量。但是,const 成员函数可以修改静态成员变量

原因:

因为静态成员变量不属于任何特定的对象。它们是类级别的,不构成对象状态的一部分。const 关键字是针对对象实例的常量性(constness),而静态成员变量是独立于对象实例的。

示例:

Point.h 中,我们声明了一个 const 成员函数 increment_static_count_via_const_func()

cpp
1
2
3
4
5
6
7
// Point.h
class Point {
// ...
public:
// ...
void increment_static_count_via_const_func() const; // 注意这里的 const
};

Point.cpp 中实现:

cpp
1
2
3
4
5
6
// Point.cpp
void Point::increment_static_count_via_const_func() const {
++m_point_count; // 这是一个 const 成员函数,但它修改了静态成员变量 m_point_count
// 这是允许的,因为 m_point_count 不属于对象状态
std::cout << "Static count incremented via const member function. New count: " << m_point_count << std::endl;
}

main.cpp 中调用:

cpp
1
2
3
4
5
6
// main.cpp
// ...
Point p1(10.0, 20.0);
// ...
p1.increment_static_count_via_const_func(); // 调用 const 函数来修改静态变量
std::cout << "通过 const 成员函数修改后,Point 对象的总数量: " << Point::get_point_count() << std::endl;

这证明了 const 成员函数可以修改静态成员变量。

6. 完整代码示例

为了让您更好地理解,我们将上面所有的代码片段组合成一个完整的项目。

Point.h:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Point.h
#ifndef POINT_H
#define POINT_H

#include <cstddef> // for size_t, typically used for counts or sizes

class Point {
private:
double m_x;
double m_y;
// 声明静态成员变量:用于跟踪 Point 对象的数量
// 注意:这里只是声明,不能初始化。
static size_t m_point_count;

public:
// 构造函数
// 委托构造函数将会在内部调用这个两参数构造函数
Point(double x, double y);
Point(double xy_coord); // 单参数构造函数,x和y坐标相同
Point(); // 默认构造函数
Point(const Point& other); // 拷贝构造函数

// 析构函数:当 Point 对象销毁时,减少计数
~Point();

// 打印 Point 对象的成员变量地址 (非静态成员变量)
void print_info() const;

// 计算点到原点的距离 (非静态成员函数)
double distance_to_origin() const;

// 静态成员函数:用于获取静态成员变量 m_point_count 的值
// 静态函数不能访问非静态成员变量 (如 m_x, m_y)
static size_t get_point_count();

// 演示 const 成员函数可以修改静态成员变量
void increment_static_count_via_const_func() const;
};

#endif // POINT_H

Point.cpp:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Point.cpp
#include "Point.h"
#include <iostream> // For std::cout, std::endl
#include <cmath> // For std::sqrt, std::pow

// 静态成员变量的定义和初始化
// 必须在类定义外部的 .cpp 文件中进行,并且需要指明所属的类 Point::
size_t Point::m_point_count = 0;

// 两参数构造函数
Point::Point(double x, double y) : m_x(x), m_y(y) {
++m_point_count; // 每次创建 Point 对象,静态计数加 1
std::cout << "[构造函数] Point(double, double) 被调用. 当前 Point 数量: " << m_point_count << std::endl;
}

// 单参数构造函数:委托给两参数构造函数
Point::Point(double xy_coord) : Point(xy_coord, xy_coord) {
// 委托构造函数已处理 m_point_count 的增量
std::cout << "[构造函数] Point(double) 被调用." << std::endl;
}

// 默认构造函数:委托给两参数构造函数
Point::Point() : Point(0.0, 0.0) {
// 委托构造函数已处理 m_point_count 的增量
std::cout << "[构造函数] Point() 被调用." << std::endl;
}

// 拷贝构造函数:委托给两参数构造函数(这里为了演示,手动复制并增加计数)
Point::Point(const Point& other) : m_x(other.m_x), m_y(other.m_y) {
++m_point_count; // 拷贝构造也算创建新对象,静态计数加 1
std::cout << "[构造函数] Point(const Point&) 拷贝构造函数被调用. 当前 Point 数量: " << m_point_count << std::endl;
}

// 析构函数
Point::~Point() {
--m_point_count; // 每次销毁 Point 对象,静态计数减 1
std::cout << "[析构函数] Point 对象被销毁. 当前 Point 数量: " << m_point_count << std::endl;
}

// 打印 Point 对象的成员变量地址
void Point::print_info() const {
std::cout << " Point 对象地址信息: [m_x 地址: " << &m_x << ", m_y 地址: " << &m_y << "]" << std::endl;
}

// 计算点到原点的距离
double Point::distance_to_origin() const {
// 使用 std::pow 计算平方,std::sqrt 计算平方根
return std::sqrt(std::pow(m_x, 2) + std::pow(m_y, 2));
}

// 静态成员函数的实现:获取静态成员变量 m_point_count 的值
size_t Point::get_point_count() {
return m_point_count;
}

// 演示 const 成员函数可以修改静态成员变量
void Point::increment_static_count_via_const_func() const {
// 尽管这个函数是 const 的,但它能修改 m_point_count,
// 因为 m_point_count 不属于任何 Point 对象的实例状态。
++m_point_count;
std::cout << " [演示] 通过 const 成员函数调用 `increment_static_count_via_const_func()`,静态计数被修改. 当前 Point 数量: " << m_point_count << std::endl;
}

main.cpp:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// main.cpp
#include "Point.h" // 包含 Point 类的定义
#include <iostream> // For std::cout, std::endl
#include <vector> // For std::vector (可选,用于演示动态创建多个对象)

int main() {
std::cout << "--- 程序开始 ---" << std::endl;

// 1. 在创建任何 Point 对象之前,访问静态成员变量
// 通过静态成员函数 Point::get_point_count() 访问,因为 m_point_count 是 private 的
std::cout << "当前 Point 对象的数量 (程序启动时): " << Point::get_point_count() << std::endl; // 预期输出: 0

std::cout << "\n--- 创建第一个 Point 对象 p1 ---" << std::endl;
Point p1(10.0, 20.0); // 创建一个 Point 对象,m_point_count 增加
std::cout << "创建 p1 后,Point 对象的数量: " << Point::get_point_count() << std::endl; // 预期输出: 1
p1.print_info(); // 打印 p1 的地址信息,显示它有独立的 m_x 和 m_y 地址

std::cout << "\n--- 创建 Point 数组 ---" << std::endl;
// 使用初始化列表创建 Point 对象数组,会调用相应的构造函数
Point points_arr[] = {
Point(1.0, 1.0), // 调用 Point(double, double)
Point(), // 调用 Point() -> 委托 Point(0.0, 0.0)
Point(4.0) // 调用 Point(double) -> 委托 Point(4.0, 4.0)
};
std::cout << "创建数组后,Point 对象的总数量: " << Point::get_point_count() << std::endl; // 预期输出: 1 + 3 = 4

std::cout << "\n--- 数组中各 Point 对象的地址信息 ---" << std::endl;
points_arr[0].print_info();
points_arr[1].print_info();
points_arr[2].print_info();

// 比较 p1 和数组中对象的地址,说明它们是独立的对象
std::cout << "注意: p1 和 points_arr 中对象的 m_x, m_y 地址都不同,说明它们是独立的对象实例。" << std::endl;
std::cout << "但所有对象共享同一个 m_point_count 静态变量。" << std::endl;

std::cout << "\n--- 演示 const 成员函数修改静态成员变量 ---" << std::endl;
p1.increment_static_count_via_const_func(); // 调用 const 成员函数,修改静态计数
std::cout << "通过 const 成员函数修改后,Point 对象的总数量: " << Point::get_point_count() << std::endl; // 预期输出: 5

std::cout << "\n--- 计算距离 ---" << std::endl;
std::cout << "p1 到原点的距离: " << p1.distance_to_origin() << std::endl; // 预期输出: 根号(10*10 + 20*20) = 22.36

std::cout << "\n--- 程序结束 ---" << std::endl;
// 在 main 函数结束时,所有局部对象 (p1, points_arr 中的对象) 将被销毁,
// 它们的析构函数会被调用,m_point_count 将会相应减少。
return 0;
}

编译和运行:

你需要将这三个文件(Point.h, Point.cpp, main.cpp)放在同一个文件夹中。然后使用 C++ 编译器(如 g++)进行编译:

bash
1
2
g++ Point.cpp main.cpp -o my_point_app -std=c++17 # -std=c++17 可选,确保编译器支持 C++17 或更高版本
./my_point_app

运行结果会清晰地展示 m_point_count 如何随着对象的创建和销毁而变化,以及它在 main 函数运行前就已经初始化,并且 const 成员函数可以修改它的特性。

4. QA 闪卡 (QA Flash Cards)

Q1: 什么是静态成员变量?它与普通成员变量有什么主要区别?
A1: 静态成员变量是属于类本身的变量,不依附于任何特定的对象。所有对象共享这同一个副本。而普通成员变量属于每个对象,每个对象都有自己的独立副本。

Q2: 如何声明一个静态成员变量?
A2: 在类定义中,在成员变量类型前加上 static 关键字。例如:static int my_static_var;

Q3: 静态成员变量应该在哪里进行初始化?为什么不能在类定义中初始化?
A3: 静态成员变量必须在类定义之外的 .cpp 源文件中进行定义和初始化。例如:int MyClass::my_static_var = 0;。不能在类定义中初始化是因为避免重复定义的问题,头文件可能被多个源文件包含。

Q4: 静态成员变量是在程序的哪个阶段被初始化的?
A4: 它们在 main 函数运行之前就被初始化,生命周期与整个程序相同。

Q5: const 成员函数可以修改静态成员变量吗?为什么?
A5: 可以。因为 const 成员函数只承诺不修改调用它的对象的非静态成员变量。静态成员变量不属于任何特定对象的状态,而是属于类本身,因此不受对象 const 性的限制。

5. 常见误解或错误

  1. 忘记在 .cpp 文件中初始化静态成员变量: 这是一个非常常见的错误。只在 .h 文件中声明 static int my_static_var; 是不够的,你必须在 .cpp 文件中写上 int MyClass::my_static_var = 0;。否则,链接器会报错,因为它找不到该变量的定义。
  2. .h 文件(类定义内部)尝试初始化非 const 静态成员变量: 除非你使用的是 C++17 的 inline 关键字,否则在类定义内部直接初始化 static int my_static_var = 0; 会导致编译错误。这是因为旧版本 C++ 规定,非 const 静态成员变量的定义和初始化必须在外部。
  3. 认为静态成员变量是每个对象独立拥有的: 这是对“静态”最核心的误解。静态成员变量是所有对象共享的,只有一个副本。
  4. 在静态成员函数中访问非静态成员变量: 静态成员函数不接收 this 指针,因此它们无法直接访问类的非静态成员变量(如 m_x, m_y),因为这些变量是与特定对象实例绑定的。
  5. const 成员函数修改静态成员变量感到困惑: 这是对 const 语义理解不深造成的。记住 const 是针对对象实例的,不是针对类的。

6. 编程练习

现在,是时候动手实践一下了!

任务:

我们将扩展 Point 类。除了 m_point_count 之外,我们还希望追踪 当前活动(即未被销毁)的 Point 对象的总面积。假设每个 Point 对象代表一个边长为 m_x 的正方形。我们需要一个静态成员变量来累加所有活动 Point 对象的面积。

请完成以下代码片段,实现这个功能:

Point.h (已提供部分代码,你需要添加新的静态成员声明):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Point.h
#ifndef POINT_EXERCISE_H
#define POINT_EXERCISE_H

#include <cstddef> // for size_t

class Point {
private:
double m_x;
double m_y;
static size_t m_point_count;

// TODO 1: 在这里声明一个新的静态成员变量 m_total_area,类型为 double。
// 它应该追踪所有活动 Point 对象的总面积。

public:
Point(double x, double y);
Point(double xy_coord);
Point();
Point(const Point& other);
~Point();

void print_info() const;
double distance_to_origin() const;

static size_t get_point_count();
void increment_static_count_via_const_func() const;

// TODO 2: 在这里声明一个新的静态成员函数 get_total_area(),返回 m_total_area 的值。
// 该函数应该是静态的,因为 m_total_area 是静态成员变量。
};

#endif // POINT_EXERCISE_H

Point.cpp (已提供部分代码,你需要完成 TODO 部分):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Point.cpp
#include "Point.h"
#include <iostream>
#include <cmath>

// 静态成员变量 m_point_count 的定义和初始化
size_t Point::m_point_count = 0;

// TODO 3: 在这里定义和初始化新的静态成员变量 m_total_area 为 0.0。

// 两参数构造函数
Point::Point(double x, double y) : m_x(x), m_y(y) {
++m_point_count;
// TODO 4: 在构造函数中,将当前 Point 对象的面积 (m_x * m_x) 加到 m_total_area。
std::cout << "[构造函数] Point(double, double) 被调用. 当前 Point 数量: " << m_point_count << std::endl;
}

// 拷贝构造函数
Point::Point(const Point& other) : m_x(other.m_x), m_y(other.m_y) {
++m_point_count;
// TODO 5: 在拷贝构造函数中,同样将当前 Point 对象的面积 (m_x * m_x) 加到 m_total_area。
std::cout << "[构造函数] Point(const Point&) 拷贝构造函数被调用. 当前 Point 数量: " << m_point_count << std::endl;
}

// 析构函数
Point::~Point() {
--m_point_count;
// TODO 6: 在析构函数中,将当前 Point 对象的面积 (m_x * m_x) 从 m_total_area 中减去。
std::cout << "[析构函数] Point 对象被销毁. 当前 Point 数量: " << m_point_count << std::endl;
}

// TODO 7: 实现静态成员函数 get_total_area(),返回 m_total_area 的值。


// 其他构造函数和成员函数保持不变 (为简洁省略)
Point::Point(double xy_coord) : Point(xy_coord, xy_coord) { /* ... */ }
Point::Point() : Point(0.0, 0.0) { /* ... */ }
void Point::print_info() const { /* ... */ }
double Point::distance_to_origin() const { /* ... */ }
size_t Point::get_point_count() { return m_point_count; }
void Point::increment_static_count_via_const_func() const { ++m_point_count; std::cout << " [演示] 通过 const 成员函数调用 `increment_static_count_via_const_func()`,静态计数被修改. 当前 Point 数量: " << m_point_count << std::endl; }

main.cpp (你需要在这里测试你的实现):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// main.cpp
#include "Point.h"
#include <iostream>

int main() {
std::cout << "--- 练习开始 ---" << std::endl;

std::cout << "初始 Point 数量: " << Point::get_point_count()
<< ", 初始总面积: " << Point::get_total_area() << std::endl; // TODO 8: 调用 get_total_area()

Point p1(5.0, 5.0); // 边长为 5 的正方形
std::cout << "创建 p1 (边长 5.0) 后,Point 数量: " << Point::get_point_count()
<< ", 总面积: " << Point::get_total_area() << std::endl; // 预期总面积 25.0

Point p2(10.0, 10.0); // 边长为 10 的正方形
std::cout << "创建 p2 (边长 10.0) 后,Point 数量: " << Point::get_point_count()
<< ", 总面积: " << Point::get_total_area() << std::endl; // 预期总面积 25.0 + 100.0 = 125.0

{ // 创建一个局部作用域,让 p3 在这里销毁
Point p3(2.0, 2.0); // 边长为 2 的正方形
std::cout << "创建 p3 (边长 2.0) 后,Point 数量: " << Point::get_point_count()
<< ", 总面积: " << Point::get_total_area() << std::endl; // 预期总面积 125.0 + 4.0 = 129.0
} // p3 在此作用域结束时被销毁

std::cout << "p3 销毁后,Point 数量: " << Point::get_point_count()
<< ", 总面积: " << Point::get_total_area() << std::endl; // 预期总面积 129.0 - 4.0 = 125.0

std::cout << "--- 练习结束 ---" << std::endl;
return 0;
}

请您在提供的代码中填入 TODO 标记处的代码,然后编译并运行,看看是否能得到预期的结果。完成后,请将您的解决方案发给我!


🧑‍💻 User

Point.h:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Point.h
#ifndef POINT_EXERCISE_H
#define POINT_EXERCISE_H

#include <cstddef> // for size_t

class Point {
private:
double m_x;
double m_y;
static size_t m_point_count;

static double m_total_area;

// TODO 1: 在这里声明一个新的静态成员变量 m_total_area,类型为 double。
// 它应该追踪所有活动 Point 对象的总面积。

public:
Point(double x, double y);
Point(double xy_coord);
Point();
Point(const Point& other);
~Point();

void print_info() const;
double distance_to_origin() const;

static size_t get_point_count();
void increment_static_count_via_const_func() const;


// TODO 2: 在这里声明一个新的静态成员函数 get_total_area(),返回 m_total_area 的值。
// 该函数应该是静态的,因为 m_total_area 是静态成员变量。
static double get_total_area();
};

#endif // POINT_EXERCISE_H

Point.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Point.cpp
#include "Point.h"
#include <iostream>
#include <cmath>

// 静态成员变量 m_point_count 的定义和初始化
size_t Point::m_point_count = 0;

// TODO 3: 在这里定义和初始化新的静态成员变量 m_total_area 为 0.0。
double Point::m_total_area = 0.0;

// 两参数构造函数
Point::Point(double x, double y) : m_x(x), m_y(y) {
++m_point_count;
// TODO 4: 在构造函数中,将当前 Point 对象的面积 (m_x * m_x) 加到 m_total_area。
m_total_area += m_x * m_y;
std::cout << "[构造函数] Point(double, double) 被调用. 当前 Point 数量: " << m_point_count << std::endl;
}

// 拷贝构造函数
Point::Point(const Point& other) : m_x(other.m_x), m_y(other.m_y) {
++m_point_count;
// TODO 5: 在拷贝构造函数中,同样将当前 Point 对象的面积 (m_x * m_x) 加到 m_total_area。
m_total_area += m_x * m_y;
std::cout << "[构造函数] Point(const Point&) 拷贝构造函数被调用. 当前 Point 数量: " << m_point_count << std::endl;
}

// 析构函数
Point::~Point() {
--m_point_count;
// TODO 6: 在析构函数中,将当前 Point 对象的面积 (m_x * m_x) 从 m_total_area 中减去。
m_total_area -= m_x * m_y;
std::cout << "[析构函数] Point 对象被销毁. 当前 Point 数量: " << m_point_count << std::endl;
}

// TODO 7: 实现静态成员函数 get_total_area(),返回 m_total_area 的值。
double Point::get_total_area() {
return m_total_area;
}

// 其他构造函数和成员函数保持不变 (为简洁省略)
Point::Point(double xy_coord) : Point(xy_coord, xy_coord) { /* ... */ }
Point::Point() : Point(0.0, 0.0) { /* ... */ }
void Point::print_info() const { /* ... */ }
double Point::distance_to_origin() const { /* ... */ }
size_t Point::get_point_count() { return m_point_count; }
void Point::increment_static_count_via_const_func() const { ++m_point_count; std::cout << " [演示] 通过 const 成员函数调用 `increment_static_count_via_const_func()`,静态计数被修改. 当前 Point 数量: " << m_point_count << std::endl; }

🤖 Assistant

恭喜您,您的解决方案非常棒!您已经掌握了静态成员变量的核心概念及其使用方法。


C++ 静态成员变量练习:您的解决方案评估

总体评价:

您的解决方案非常出色!您准确地理解了如何在头文件 (.h) 中声明静态成员变量和静态成员函数,以及如何在源文件 (.cpp) 中定义和初始化它们。您也正确地在构造函数中增加了总面积,并在析构函数中减少了总面积,这表明您对静态成员变量作为类级别共享数据的理解非常到位。

唯一需要微调的地方:

在面积计算的部分,您根据 m_x * m_y 来计算面积。根据练习的描述,”假设每个 Point 对象代表一个边长为 m_x 的正方形“,这意味着面积应该是 m_x 的平方,即 m_x * m_x。这是一个小小的逻辑错误,与静态成员变量本身的知识点无关,更多是关于问题描述的理解。

让我们来详细看看具体的修改点。


正确答案与详细解释

1. Point.h 文件

您的 Point.h 代码完全正确,成功声明了新的静态成员变量 m_total_area 和静态成员函数 get_total_area()

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Point.h
#ifndef POINT_EXERCISE_H
#define POINT_EXERCISE_H

#include <cstddef> // for size_t

class Point {
private:
double m_x;
double m_y;
static size_t m_point_count;

// TODO 1: 在这里声明一个新的静态成员变量 m_total_area,类型为 double。
static double m_total_area; // 正确!声明静态成员变量。

public:
Point(double x, double y);
Point(double xy_coord);
Point();
Point(const Point& other);
~Point();

void print_info() const;
double distance_to_origin() const;

static size_t get_point_count();
void increment_static_count_via_const_func() const;

// TODO 2: 在这里声明一个新的静态成员函数 get_total_area(),返回 m_total_area 的值。
static double get_total_area(); // 正确!声明静态成员函数。
};

#endif // POINT_EXERCISE_H

解释:

  • static double m_total_area;:这行代码在类内部声明了一个名为 m_total_areadouble 类型静态成员变量。static 关键字表示它属于 Point 类本身,而不是 Point 类的任何特定对象。
  • static double get_total_area();:这行代码声明了一个名为 get_total_area 的静态成员函数。由于它将访问静态成员变量 m_total_area,并且与特定对象实例无关,因此将其声明为 static 是非常合适的。

2. Point.cpp 文件

除了面积计算的逻辑外,您的 Point.cpp 也非常接近完美。

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Point.cpp
#include "Point.h"
#include <iostream>
#include <cmath>

// 静态成员变量 m_point_count 的定义和初始化
size_t Point::m_point_count = 0;

// TODO 3: 在这里定义和初始化新的静态成员变量 m_total_area 为 0.0。
double Point::m_total_area = 0.0; // 正确!在类外定义并初始化静态成员变量。

// 两参数构造函数
Point::Point(double x, double y) : m_x(x), m_y(y) {
++m_point_count;
// TODO 4: 在构造函数中,将当前 Point 对象的面积 (m_x * m_x) 加到 m_total_area。
// 修改点:面积应为 m_x * m_x,因为题目假设是“边长为 m_x 的正方形”
m_total_area += m_x * m_x; // <-- 修正此处
std::cout << "[构造函数] Point(double, double) 被调用. 当前 Point 数量: " << m_point_count << std::endl;
}

// 拷贝构造函数
Point::Point(const Point& other) : m_x(other.m_x), m_y(other.m_y) {
++m_point_count;
// TODO 5: 在拷贝构造函数中,同样将当前 Point 对象的面积 (m_x * m_x) 加到 m_total_area。
// 修改点:面积应为 m_x * m_x
m_total_area += m_x * m_x; // <-- 修正此处
std::cout << "[构造函数] Point(const Point&) 拷贝构造函数被调用. 当前 Point 数量: " << m_point_count << std::endl;
}

// 析构函数
Point::~Point() {
--m_point_count;
// TODO 6: 在析构函数中,将当前 Point 对象的面积 (m_x * m_x) 从 m_total_area 中减去。
// 修改点:面积应为 m_x * m_x
m_total_area -= m_x * m_x; // <-- 修正此处
std::cout << "[析构函数] Point 对象被销毁. 当前 Point 数量: " << m_point_count << std::endl;
}

// TODO 7: 实现静态成员函数 get_total_area(),返回 m_total_area 的值。
double Point::get_total_area() {
return m_total_area; // 正确!返回静态成员变量的值。
}

// 其他构造函数和成员函数保持不变 (为简洁省略)
Point::Point(double xy_coord) : Point(xy_coord, xy_coord) { /* ... */ }
Point::Point() : Point(0.0, 0.0) { /* ... */ }
void Point::print_info() const { /* ... */ }
double Point::distance_to_origin() const { /* ... */ }
size_t Point::get_point_count() { return m_point_count; }
void Point::increment_static_count_via_const_func() const { ++m_point_count; std::cout << " [演示] 通过 const 成员函数调用 `increment_static_count_via_const_func()`,静态计数被修改. 当前 Point 数量: " << m_point_count << std::endl; }

解释:

  • double Point::m_total_area = 0.0;:这行代码在 Point.cpp 文件中定义并初始化了 m_total_area 静态成员变量。这是 C++ 中为静态成员变量分配存储空间并赋予初始值的标准做法。
  • m_total_area += m_x * m_x;:在构造函数中,我们增加了 m_total_area。根据题目要求,每个 Point 代表一个边长为 m_x 的正方形,所以面积应该是 m_x 的平方。
  • m_total_area -= m_x * m_x;:在析构函数中,当 Point 对象被销毁时,我们需要将它所贡献的面积从 m_total_area 中减去,以保持总面积的准确性。

3. main.cpp 文件

以下是修改后的 main.cpp,用于测试您的实现,并提供预期输出:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// main.cpp
#include "Point.h"
#include <iostream>

int main() {
std::cout << "--- 练习开始 ---" << std::endl;

std::cout << "初始 Point 数量: " << Point::get_point_count()
<< ", 初始总面积: " << Point::get_total_area() << std::endl; // TODO 8: 调用 get_total_area()
// 预期输出: 初始 Point 数量: 0, 初始总面积: 0

Point p1(5.0, 5.0); // 边长为 5 的正方形,面积 25.0
std::cout << "创建 p1 (边长 5.0) 后,Point 数量: " << Point::get_point_count()
<< ", 总面积: " << Point::get_total_area() << std::endl;
// 预期输出: Point 数量: 1, 总面积: 25.0

Point p2(10.0, 10.0); // 边长为 10 的正方形,面积 100.0
std::cout << "创建 p2 (边长 10.0) 后,Point 数量: " << Point::get_point_count()
<< ", 总面积: " << Point::get_total_area() << std::endl;
// 预期输出: Point 数量: 2, 总面积: 125.0 (25.0 + 100.0)

{ // 创建一个局部作用域,让 p3 在这里销毁
Point p3(2.0, 2.0); // 边长为 2 的正方形,面积 4.0
std::cout << "创建 p3 (边长 2.0) 后,Point 数量: " << Point::get_point_count()
<< ", 总面积: " << Point::get_total_area() << std::endl;
// 预期输出: Point 数量: 3, 总面积: 129.0 (125.0 + 4.0)
} // p3 在此作用域结束时被销毁,析构函数被调用

std::cout << "p3 销毁后,Point 数量: " << Point::get_point_count()
<< ", 总面积: " << Point::get_total_area() << std::endl;
// 预期输出: Point 数量: 2, 总面积: 125.0 (129.0 - 4.0)

std::cout << "--- 练习结束 ---" << std::endl;
return 0;
}

编译与运行:

bash
1
2
g++ Point.cpp main.cpp -o area_tracker
./area_tracker

运行上述代码,您将看到 m_total_area 随着对象的创建和销毁而正确地增加和减少。


进一步学习与思考

您已经很好地掌握了 C++ 静态成员变量的基础知识。为了更深入地理解并应对实际开发中的挑战,我建议您继续探索以下主题:

  1. C++17 inline 静态成员变量:
    • 在 C++17 及更高版本中,你可以使用 inline 关键字在类定义(即头文件)内部直接初始化静态成员变量,例如 static inline double m_total_area = 0.0;。这简化了代码,避免了在 .cpp 文件中单独初始化。了解它的用途和何时使用它。
  2. const 静态成员变量的特殊性:
    • 对于 const 且为整型(包括枚举类型)的静态成员变量,即使在 C++17 之前,也可以在类定义内部进行就地(in-class)初始化。例如:static const int MAX_POINTS = 100;。了解这种特例以及它的应用场景。
  3. 单例模式(Singleton Pattern)与静态成员:
    • 静态成员变量和静态成员函数在实现单例模式时非常有用,单例模式确保一个类只有一个实例,并提供一个全局访问点。这对于需要全局唯一资源的场景非常有用,例如日志管理器、配置读取器等。
  4. 静态成员变量的线程安全性(Thread Safety):
    • 当程序涉及多线程时,多个线程可能会同时尝试访问和修改静态成员变量。这可能导致数据竞争(race condition)和不可预测的行为。了解如何使用互斥量(mutex)或其他同步机制来确保静态成员变量在多线程环境下的安全访问。

希望这些扩展问题能帮助您进一步巩固和拓展您的 C++ 知识!保持学习的热情,您会越来越棒!


评论